import asyncio
from traceback import format_exc
from datetime import datetime
import time
from traceback import format_exc
from threading import Thread, RLock
from types import FunctionType, CodeType
from typing import Callable, Optional

from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload
from loguru import logger

from core.client_creator import ClientMap
from core.database import getDB
from config import settings
from schemas.deviceBase import Point, PointGroup, DeviceModel 
from schemas.device import PointSchema
from schemas.script import ScriptRunSchema
from models.device import DeviceTable, PLCPointTable
from utils.iot_protocol import ProtocolMaster, protocolSelections

DEV_GOOD = 1
DEV_WARNING = 2
DEV_BREAK_DOWN = 3

global DeviceModelMap

DeviceModelMap: dict[str, "DeviceMonitor"] = {}  # 缓存设备采集线程对象


class ScriptObj:
    def __init__(self, data: ScriptRunSchema) -> None:
        func = compile(source=data.content, filename="", mode="exec")  # 将字符串转换为compile对象
        _main = None
        for f in func.co_consts:    # 遍历字符串内存在的所有变量和对象
            if f and isinstance(f, CodeType) and f.co_name and f.co_name == "main":
                _main = FunctionType(f, {"now": datetime.now})
        if not _main:
            raise AttributeError("脚本函数中不存在main函数作为入口")
        self.func = _main
        self.clients = data.clients

    def run(self, devModel):
        res = self.func(devModel)
        for cli in [*self.clients]:
            if ClientMap.get(cli):
                if res is None:
                    return
                if isinstance(res, dict) and res.get("topic") and res.get("payload"):
                    ClientMap[cli].send(**res)
                else:
                    raise AttributeError("对接MQTT客户端的脚本返回值必须为字典且包含\"topic\"和\"payload\"字段")
            else:
                raise AttributeError(f"执行失败，{cli}客户端不存在，可能已被销毁")


def pointsSlice(points: list[Point]) -> list[list[Point]]:
    """ 点位切片函数  将超限的点位分组存储，避免采集点位过多导致通讯失败 """
    length = len(points)
    for i in range(1, length):
        if points[i].addr - points[0].addr >= settings.MAX_PLC_READ_LEN:
            return [points[:i]] + pointsSlice(points[i:])
    return [points]


def getDeviceModel(no: str):
    """ 根据设备资产号获取设备模型对象 """
    dev = DeviceModelMap.get(no)
    if not dev:
        return None
    return DeviceModelMap[no]


def createDeviceModel(
        no: str, name: str, 
        protocol: str, 
        srcPoints: list[PointSchema], 
        srcScripts: list[ScriptRunSchema],
        host: str, port: int
    ):
    """ 创建设备模型对象 """
    if DeviceModelMap.get(no):
        DeviceModelMap[no].stop()
    dev = DeviceMonitor(no, name, protocol, srcPoints, srcScripts, host, port)
    # 将设备注册进设备模型映射表中
    DeviceModelMap[no] = dev
    dev.start()
    return dev


def removeDeviceModel(no: Optional[str] = None):
    """ 根据设备资产号删除设备模型对象，不指定设备时直接停止所有设备采集 """
    if not no:
        keys = [k for k in DeviceModelMap]
        for k in DeviceModelMap:
            DeviceModelMap[k].stop()
        for k in keys:
            del DeviceModelMap[k]
    else:
        dev = DeviceModelMap.get(no)
        if dev:
            # raise AttributeError(f"{no}设备模型不存在")
            del DeviceModelMap[no]
            dev.stop()


def reloadDeviceModel(no: str):
    """ 重新加载设备信息 """
    removeDeviceModel(no)
    with getDB() as db:
        dev = db.scalars(select(DeviceTable).options(
            selectinload(DeviceTable.netInterface),
            selectinload(DeviceTable.points),
            selectinload(DeviceTable.scripts)
        ).where(DeviceTable.no == no)).one_or_none()
    if dev:
        createDeviceModel(
            no=dev.no, name=dev.name, protocol=dev.communicationId, 
            host=dev.netInterface.host, port=dev.netInterface.port,
            srcPoints=[PointSchema(**p.dict(exclude={"deviceNo"})) for p in dev.points],
            srcScripts=[ScriptRunSchema(content=s.content, clients=[]) for s in dev.scripts]
        )


class DeviceMonitor(Thread):
    """ PLC设备数据监听对象 """
    def __init__(
        self, 
        no: str, name: str, protocol: str, 
        srcPoints: list[PointSchema], 
        srcScripts: list[ScriptRunSchema],
        host: str, port: int
    ) -> None:
        super().__init__(daemon=True)
        self.no = no
        self.name = name
        self.com: ProtocolMaster
        self.scripts: list[ScriptObj] = []
        self.testScripts: dict[str, dict] = {}      # 用户测试的脚本
        self.serializePoints: list[list[Point]] = []    # 序列化后的点位
        self.linearPoints: list[Point] = [] # 一维的点位集合，方便生命周期中遍历
        self.model: DeviceModel   # 暴露设备模型供用户脚本调用
        self.status = DEV_BREAK_DOWN
        self.listening = False
        self.logs: list = []
        try:
            # 初始化物理通讯端口
            self.initCommunication(protocol, host=host, port=port)
            # 将点位进行结构化处理
            self.initPoints(srcPoints)
            # 初始化脚本
            self.initScripts(srcScripts)
        except Exception:
            logger.error(format_exc())
            self.addLog(format_exc())

    def addLog(self, info: str | Exception):
        while len(self.logs) > 20:
            self.logs.pop()
        _info = f"{str(type(info))[8:-2]}: {str(info)}" if isinstance(info, Exception) else info
        if len(self.logs) > 0 and self.logs[0]["content"] == _info:
            self.logs[0]["time"] = datetime.now()
        else:
            self.logs.insert(0, {
                "time": datetime.now(),
                "content": _info
            })

    def initCommunication(self, protocol: str, host: str, port: int):
        """ 初始化通信端口 """
        # dev = db.scalars(select(DeviceTable).options(
        #     selectinload(DeviceTable.communication
        # )).where(DeviceTable.no == self.no)).one_or_none()
        # if not dev:
        #     raise ValueError("设备不存在")
        # protocolName = dev.communication.protocol
        self.com = protocolSelections[protocol]["driver"](host=host, port=port)
    
    def initPoints(self, points: list[PointSchema]):
        # 从数据库中读取设备点位
        # points = db.scalars(select(PLCPointTable).where(PLCPointTable.deviceNo == self.no)).fetchall()
        # 将点位按照分组进行缓存，注意这里的点必须使用对象引用，不进行深拷贝
        groupMap = {}
        for p in points:
            if not groupMap.get(p.group):
                groupMap[p.group] = []
            newPoint = Point(**p.model_dump(include={"name", "func", "addr", "desc"}))
            groupMap[p.group].append(newPoint)
            self.linearPoints.append(newPoint)
        # 初始化设备点位模型
        self.model = DeviceModel(
            self.no, self.name, 
            [PointGroup(points=group, name=name) for name, group in groupMap.items()]
        )
        # 序列化点位，方便读写
        funcDict: dict[str, list[Point]] = {}   # 点位分类缓存
        # 根据功能码对点位进行分类
        for _, ps in groupMap.items():
            for p in ps:
                if not funcDict.get(p.func):
                    funcDict[p.func] = []
                funcDict[p.func].append(p)
        # 对点位进行排序并按照最大长度进行切片
        for _, ps in funcDict.items():
            ps.sort(key=lambda p: p.addr)
            self.serializePoints += pointsSlice(ps)
    
    def insertTestScript(self, tag: str, func: Callable):
        self.testScripts[tag] = {
            "func": func,
            "queue": []
        }
        return self.testScripts[tag]

    def removeTestScript(self, tag: str):
        if self.testScripts.get(tag):
            del self.testScripts[tag]

    def runTestScript(self):
        for _, script in self.testScripts.items():
            try:
                res = script["func"](self.model)
                if len(script["queue"]) > 100:
                    script["queue"] = []
            except Exception as e:
                res = e
                print(type(e))
            if res:
                script["queue"].append(res)
    
    def initScripts(self, scripts: list[ScriptRunSchema]):
        """ 加载用户脚本 """
        self.scripts = [ScriptObj(data=script) for script in scripts]

    def beforeListen(self):
        self.listening = True
        # 首次监控需要保证设备连接正常，即通讯状态至少为 DEV_WARNING 以下
        while self.status > DEV_WARNING and self.listening:
            self.update()
            time.sleep(0)
        for p in self.linearPoints:
            p.beforeListen()
    
    def beforeUpdate(self):
        for p in self.linearPoints:
            p.beforeUpdate()

    def runScript(self):
        for script in self.scripts:
            try:
                script.run(self.model)
            except Exception as e:
                self.addLog(str(e))

    def update(self):
        breakTimes = 0
        for points in self.serializePoints:
            # 获取点位功能码
            func = points[0].func   
            # 获取最小点位的地址
            addr = points[0].addr   
            # 最大点位与最小点位地址差值+1得到读取的数据长度
            size = points[-1].addr - addr + 1   
            try:
                # 调用read函数读取点位值列表
                result = self.com.read(func, addr, size)    
                if not result:
                    breakTimes += 1
                    for point in points:
                        point.value = None
                else:
                    for point in points:
                        point.value = result[point.addr - addr]
            except Exception as e:
                if size > 1:
                    self.addLog(f"<{func}{addr}>至<{func}{addr + size - 1}>间点位存在异常：{e}")
                else:
                    self.addLog(f"<{func}{addr}>点位存在异常：{e}")
                breakTimes += 1
                for point in points:
                    point.value = None
        if not self.serializePoints:
            self.status = DEV_BREAK_DOWN
        elif breakTimes == 0:
            self.status = DEV_GOOD  # 所有点位均可采集，设备通讯正常
        elif breakTimes < len(self.serializePoints):
            self.status = DEV_WARNING   # 部分点位采集失败
        else:
            self.status = DEV_BREAK_DOWN    # 设备完全无法通讯

    def updated(self):
        for p in self.linearPoints:
            p.updated()

    def stop(self):
        self.beforeStop()
        self.listening = False

    def beforeStop(self):
        pass

    def stopped(self):
        self.com.disconnect()

    def run(self) -> None:
        self.beforeListen()
        while self.listening:
            self.beforeUpdate()
            self.update()   # 执行数据读取，并更新点位值
            self.updated()
            self.runScript()
            self.runTestScript()
            time.sleep(0)
        self.stopped()

